跳到主要内容

SpringSecurity OAuth2 的各种概念

各种概念

下面这篇博客基本把各种概念都讲了

微服务架构之安全认证:JWT 令牌和 SSO

SpringSecurity 和 OAuth 配置点

注意,SpringSecurity OAuth 只负责配置 OAuth 这块的内容,它和 SpringSecurity 是分开的两个配置,而配置 SpringSecurity 的部分就不讲了(看上面提供的源码),和传统的方式是一样的

点进它们两个配置类也可以看到注册端点也不同

SpringSecurity 的配置端点是这个 WebSecurityConfigurerAdapter 抽象类

OAuth 的配置端点是这个 AuthorizationServerConfigurerAdapter 抽象类

为什么使用 OAuth

在微服务中,一般都会有一个专门的认证服务,在这个认证服务上完成认证后就可以直接访问其他服务,这种效果仅使用 SpringSecurity 是比较难实现的,所以使用一个 OAuth 服务专门用来认证,然后返回一个 Token,资源服务拿到这个 Token 后再来鉴权

这里还有一个问题 OAuth 主要负责第三方授权,那自己的服务如何授权呢?答: 自己的服务和第三方一样的认证方式

OAuth2 模式该如何选型?

首先明确一下 OAuth 的各种概念

  • 资源所有者:可以授予受保护资源的最终用户。
  • 客户端:代表资源所有者请求访问受保护资源的应用程序。
  • 资源服务器:持有受保护资源的服务,也就是你要访问的 api 接口。
  • 授权服务器:认证资源所有者以及在获得正确的认证之后颁发 access_token 的服务。
  • 用户代理:是一种被资源所有者去和客户端产生交互的代理(例如浏览器)。

这里参考极客空间的图 参考视频 OAuth2 模式该如何选型

把客户应用分成两类,一类是在客户手中的公开类型(APP、Web 之类的),另一类是服务端的

下面介绍各种模式它们适用的场景(这里密码模式就是自己的 APP 使用)

根据流程选型

数据库表是什么?

经常看到使用 OAuth2.0 都需要创建几个很复杂的表,那这个表是怎么使用的呢?

如下所示:

-- ----------------------------
-- 存放 oauth 的认证信息
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`
(
`client_id` varchar(255) NOT NULL COMMENT '客户端标识',
`resource_ids` varchar(255) NULL DEFAULT NULL COMMENT '接入资源列表',
`client_secret` varchar(255) NULL DEFAULT NULL COMMENT '客户端秘钥',
`scope` varchar(255) NULL DEFAULT NULL COMMENT '客户端申请的权限范围',
`authorized_grant_types` varchar(255) NULL DEFAULT NULL COMMENT '客户端支持的 grant_type',
`web_server_redirect_uri` varchar(255) NULL DEFAULT NULL COMMENT '重定向URI',
`authorities` varchar(255) NULL DEFAULT NULL COMMENT '客户端所拥有的Spring Security的权限值,多个用逗号(,)分隔',
`access_token_validity` int(11) NULL DEFAULT NULL COMMENT '访问令牌有效时间值(单位:秒)',
`refresh_token_validity` int(11) NULL DEFAULT NULL COMMENT '更新令牌有效时间值(单位:秒)',
`additional_information` longtext NULL DEFAULT NULL COMMENT '预留字段',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
`archived` tinyint(4) NULL DEFAULT NULL,
`trusted` tinyint(4) NULL DEFAULT NULL,
`autoapprove` varchar(255) NULL DEFAULT NULL COMMENT '用户是否自动Approval操作',
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB
CHARACTER SET = utf8
COLLATE = utf8_general_ci COMMENT = '接入客户端信息'
ROW_FORMAT = Dynamic;

实际上,OAuth 里面已经封装好了读取这个表的工具(JdbcClientDetailsService),一般都无需自己手动调用 Dao 去调用,只需按照 spring-oauth2.0 提供的标准字段创建一个这样的表就会自动工作了

点击进 JdbcClientDetailsService 这个工具类里面看它的源码

补充:设置 Token

这里配置的 Bean 主要在下面的 AuthorizationServerConfigurerAdapter 的配置端点会用到

@Configuration
public class TokenConfig {

private static final String SIGNING_KEY = "SIGNING_KEY"; //对称加密的密钥

@Bean
public TokenStore tokenStore() {
// JWT 令牌方案
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}

AuthorizationServerConfigurerAdapter 注册端点

就像 SpringSecurity 一样, OAuth2.0 也提供了一个注册端点(适配器模式),用户只需在这个适配器上配置自己想要的配置就能用了,所以本质也就是实现 各个接口,最终统一丢到 configure 里面就行了

所以这种设计就很巧妙,它不管你是怎么具体实现的,你只需给它想要的接口实现类就行了

注意,得在 yml 里面配置允许同名覆盖,否则下面的 ClientDetailsService 会无法覆盖

spring:
main:
allow-bean-definition-overriding: true

配置的方式具体看注释

注意:这里不能使用构造方法的方式注入,否则会出现循环依赖的问题,因为这里 AuthorizationCodeServices 和 ClientDetailsService 都是在本类中实例化的

package com.alsritter.mapuaa.config;

import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.sql.DataSource;
import java.util.Collections;

/**
* @author alsritter
* @version 1.0
**/
@Configuration
@EnableAuthorizationServer
@Setter(onMethod_ = {@Autowired})
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

private AuthenticationManager authenticationManager; //从WebSecurityConfig中获取的
private AuthorizationCodeServices authorizationCodeServices; //本类中的,授权码模式需要
private TokenStore tokenStore; //TokenConfig中的
private PasswordEncoder passwordEncoder; //从WebSecurityConfig中获取的
private ClientDetailsService clientDetailsService; //本类中的
private JwtAccessTokenConverter jwtAccessTokenConverter; //TokenConfig中的

/**
* 从数据库中获取客户端详情,这里调用的 JdbcClientDetailsService 工具类可以自动帮忙查询,
* ClientDetailsService 的信息是从 oauth_client_details 这张表里查出来的,
* 点击工具类源码可以看到它里面执行的 SQL 就是查询 oauth_client_details 这表,
* 所以我们的数据库中只要创建出这张表,表里再添加这些字段即可。
*
* @param dataSource
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}

/**
* 设置授权码模式的授权码从数据库中获取,这个 JdbcAuthorizationCodeServices 工具类很相似,
* 这个工具类是从 oauth_code 这个表查询的
*
* @param dataSource
* @return 返回一个授权码
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);
}

/**
* 令牌管理服务
* <p>
* 为了方便管理,我们使用 TokenConfig 这个类去配置 Token 相关的内容。
* 添加了 @Bean 注解将其添加到 Spring 容器后就可以在其它的类中去注入使用了。
* <p>
* 这里采用了 JWT 令牌管理方式,然后使用了对称密钥去进行加密。还有另外几种令牌管理方式:
* InMemoryTokenStore:在内存中存储令牌(默认)
* JdbcTokenStore:令牌存储在数据库中
* RedisTokenStore:令牌存储在 Redis 中
*
* @return 返回一个令牌
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
// DefaultTokenServices 使用随机 UUID 值作为访问令牌和刷新令牌值的令牌服务的基本实现。
// 它的自定义的主要扩展点是 TokenEnhancer ,它将在生成访问和刷新令牌之后但在存储之前调用。
// 存储方式 被委托给 TokenStore 实现,并将访问令牌的定制委托给 TokenEnhancer。
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService); //客户端信息服务
service.setSupportRefreshToken(true); //支持自动刷新

// 设置它的存储方式(这个 tokenStore 是上面传入的 TokenConfig 配置的 JWT)
service.setTokenStore(tokenStore);

// 令牌增强,这里可以设置令牌的加密方式之类的操作
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
// 设置为在 TokenConfig 配置的对称加密
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(jwtAccessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);

service.setAccessTokenValiditySeconds(7200); //令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); //刷新令牌默认有效期3天
return service;
}

/**
* 用来配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll") // /oauth/token_key 提供公有密匙的端点 允许任何人访问
.checkTokenAccess("permitAll") // /oauth/check_token 用于资源服务访问的令牌解析端点 允许任何人访问
.allowFormAuthenticationForClients(); //表单认证(申请令牌)
}

/**
* 用来配置客户端详情服务,客户端详情信息在这里进行初始化,
* 你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}

/**
* 用来配置令牌(token)的访问端点(url)和令牌服务(token services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager) //认证管理器,密码模式需要
.authorizationCodeServices(authorizationCodeServices) //授权码服务,授权码模式需要
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST); //允许post提交
}

}

补充:keytool 生成令牌

在 RSA 那篇笔记中需要自己手动写代码生成公钥私钥,这样太麻烦了,所以可以使用自带的命令行工具 keytool 生成密钥 - 更具体地说 .jks 文件:

首先 keytool 是什么? Keytool 是一个 Java 数据证书的管理工具,Keytool 将密钥(key)和证书(certificates)存在一个称为 keystore 的文件中。

在 keystore 里,包含两种数据:

  1. 密钥实体(Key entity)——密钥(secret key)又或者是私钥和配对公钥(采用非对称加密)
  2. 可信任的证书实体(trusted certificate entries)——只包含公钥

具体使用如下:

1、生成 .jks 文件

keytool -genkey -alias ccc -keyalg RSA -validity 36500 -keystore jwt-key.jks
  • 其中参数 -validity 为证书有效天数,我们可以写的大写。
  • -alias 后面是证书别名
  • 输入密码的时候没有显示,就输入就行了。退格tab 等都属于密码内容。
  • 输入这个命令之后会提示你输入秘钥库的口令
  • 接着是会提示你输入:姓氏,组织单 位名称,组织名称,城市或区域名称,省市,国家、地区代码,密钥口令。
  • 确认正确输入y,回车

按提示 输入keystore 存储密码、私钥密码、个人信息,之后会生成 jwt-key.jks 文件 若不想输入参数,可提供参数:

  • storepass keystore 文件存储密码
  • keypass 私钥加解密密码
  • alias 实体别名(包括证书私钥)
  • dname 证书个人信息
  • keyalt 采用公钥算法,默认是DSA
  • keysize 密钥长度(DSA算法对应的默认算法是sha1withDSA,不支持2048长度,此时需指定RSA)
  • validity 有效期
  • keystore 指定keystore文件

如下:

# keypass 和 storepass 保持一致
$ keytool -genkey -alias jwt-key -keyalg RSA -dname "CN=Goddy,OU=unknown,O=unknown,L=Beijing,S=china,C=CN" -keypass 123456 -keystore jwt-key.jks -storepass 123456

查看公钥

# 查看公钥
keytool -list -rfc --keystore xxx-jwt.jks | openssl x509 -inform pem -pubkey

# 提示输入密码
Enter keystore password: 你的密码
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkxmTfs89sSFeRaU5+W3F
3UZ32YjReG6q529lOYWQGQkwNY8y/TFvY1pOqHdlfQNRKECXaWGKaMch7zSr8SZR
1bcIO7Ux43qfJiXC9m2KFuJM+O5dBUw87XEmeG28DqXj8OKzBRI11VAwX43dEldo
2ogbDshEyCUb3DHOyIAl7Ym32Bz1Rg7vYb3dnBOm5OEObkAuN5h60ZH85jMB4JFm
mdGZAlSzEXcVtilK36GdJuZNo4EEHv9C1iH2FKZRhQ/T3TDawsGQGPLeN9CRvQ22
XNgEmerrGnZL7n9OFuj30Foml0x5Rtq8wr7l8QPL/V6MqEol4Szoi6CFm4R+Idlf
JwIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDczCCAlugAwIBAgIEV1IotDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJj
bjEOMAwGA1UECBMFaGViZWkxETAPBgNVBAcTCHRhbmdzaGFuMREwDwYDVQQKEwhk
b25ncmlhZjERMA8GA1UECxMIZG9uZ3JpYWYxETAPBgNVBAMTCGRvbmdyaWFmMCAX
DTE5MTIxMDExMjEyM1oYDzIxMTkxMTE2MTEyMTIzWjBpMQswCQYDVQQGEwJjbjEO
MAwGA1UECBMFaGViZWkxETAPBgNVBAcTCHRhbmdzaGFuMREwDwYDVQQKEwhkb25n
cmlhZjERMA8GA1UECxMIZG9uZ3JpYWYxETAPBgNVBAMTCGRvbmdyaWFmMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkxmTfs89sSFeRaU5+W3F3UZ32YjR
eG6q529lOYWQGQkwNY8y/TFvY1pOqHdlfQNRKECXaWGKaMch7zSr8SZR1bcIO7Ux
43qfJiXC9m2KFuJM+O5dBUw87XEmeG28DqXj8OKzBRI11VAwX43dEldo2ogbDshE
yCUb3DHOyIAl7Ym32Bz1Rg7vYb3dnBOm5OEObkAuN5h60ZH85jMB4JFmmdGZAlSz
EXcVtilK36GdJuZNo4EEHv9C1iH2FKZRhQ/T3TDawsGQGPLeN9CRvQ22XNgEmerr
GnZL7n9OFuj30Foml0x5Rtq8wr7l8QPL/V6MqEol4Szoi6CFm4R+IdlfJwIDAQAB
oyEwHzAdBgNVHQ4EFgQUDCc69gdXz2v9cmRtZ+6nhwLrWjQwDQYJKoZIhvcNAQEL
BQADggEBACkCxV//DkTX/cFwwqeeCrqiBuLUKNP2Yzt8uAlXtYoUDiSNMb2aSlyr
6MWDSodVVpXuvZ/mFv0VHJ/PzSFLhh6Z4+s3UlaUrzSCWNT7CfahVQewSx4fWJNn
quF3+ZVBKgB0NGfR9JJgZR7j23uzQxIliNrRY+sp+uBDBJk2yxwmCNf/eL7IjFbw
kfyiiMEuSu+ODo/dDvcdqcm6ZRHRTO9AEf5vkYPJDmwhFNx0+txOLhoYO+SH9Ohm
FEXLOi5Bqz+xVHbxByJ5f++tYwVUJPHU5yhHQO5sdkMmw3Skl797ZEf3h2W//WDJ
0vnvwRy+TVshz8sPufZa806HRKhF4nY=
-----END CERTIFICATE-----

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore dongriaf-jwt.jks -destkeystore dongriaf-jwt.jks -deststoretype pkcs12".

2、从生成的 JKS 中导出公钥

keytool -list -rfc --keystore jwt-key.jks | openssl x509 -inform pem -pubkey
# 如果是 powershell 需要先安装 openssl,这里使用 choco 工具(安装完后要关闭终端再打开才能用 openssl)
choco install openssl

3、把 PUBLIC KEY 部分复制到 Resource Server 的 src/main/resources/public.txt

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkxmTfs89sSFeRaU5+W3F
3UZ32YjReG6q529lOYWQGQkwNY8y/TFvY1pOqHdlfQNRKECXaWGKaMch7zSr8SZR
1bcIO7Ux43qfJiXC9m2KFuJM+O5dBUw87XEmeG28DqXj8OKzBRI11VAwX43dEldo
2ogbDshEyCUb3DHOyIAl7Ym32Bz1Rg7vYb3dnBOm5OEObkAuN5h60ZH85jMB4JFm
mdGZAlSzEXcVtilK36GdJuZNo4EEHv9C1iH2FKZRhQ/T3TDawsGQGPLeN9CRvQ22
XNgEmerrGnZL7n9OFuj30Foml0x5Rtq8wr7l8QPL/V6MqEol4Szoi6CFm4R+Idlf
JwIDAQAB
-----END PUBLIC KEY-----

4、认证服务端设置

@Bean  
protected JwtAccessTokenConverter jwtTokenEnhancer() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
new ClassPathResource("jwt-key.jks"), "wcm520".toCharArray());

JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt-key"));
return converter;
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

5、资源服务端设置

@Bean  
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

如何使用密码模式

使用密码模式

刷新令牌:

refresh_token  这个填写刷新令牌
grant_type refresh_token

资源服务器解析令牌方法

  • 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式
  • 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token
  • 若授权服务器是 JWT 非对称加密,则需要请求授权服务器的 /oauth/token_key 来获取公钥 key 进行解码

OAuth 各种端点的作用

具体配置细节参考:OAuth 2.0 登录 — 高级配置

oauth/authorize 端点:AuthorizationEndpoint 用于服务请求授权 oauth/token 端点:TokenEndpoint 用于服务访问令牌的请求 oauth/token_key 端点:如果使用 JWT 令牌,则公开用于令牌验证的公钥

可用在 AuthorizationEndpoint 找到这个授权点

而 授权服务器配置

ClientDetailsServiceConfigurer:定义客户端详细信息服务的配置程序。 AuthorizationServerSecurityConfigurer:定义令牌端点上的安全约束。 AuthorizationServerEndpointsConfigurer:定义授权和令牌端点和令牌服务。

注意,授权端点 /oauth/authorize(或其映射替代方案)应该使用 Spring Security 进行保护,以便只有经过身份验证的用户才能访问,例如使用标准的 Spring Security WebSecurityConfigurer:

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll().and()
// default protection for all resources (including /oauth/authorize)
.authorizeRequests()
.anyRequest().hasRole("USER")
// ... more configuration, e.g. for form login
}

说白了 AuthorizationEndpoint 就是根据用户认证获得授权码 code

{[/oauth/authorize]}
{[/oauth/authorize],methods=[POST]

客户端根据授权码 code 从 TokenEndpoint 获取令牌 token

{[/oauth/token],methods=[GET]}
{[/oauth/token],methods=[POST]}

Reference

微服务架构之安全认证:JWT 令牌和 SSO SpringCloud OAuth2 JWT认证 多种登录方式 这篇博客,它讲的很详细了 Spring Security OAuth2 Demo —— 密码模式(Password) Spring Security OAuth2实现使用JWT